Mirror cudf.pandas class-level monkeypatches onto the real type - #23001
Conversation
Per review, split the proxy `__setattr__`/`__delattr__` monkeypatch-mirroring fix (and its tests in test_fast_slow_proxy.py) out of this PR; it now lives in NVIDIA#23001. The 14 pandas-tests it unblocks (read_excel engine selection + a monkeypatched custom accessor) are re-marked xfail here, since they require the proxy fix rather than the Excel-reader fixes. This PR keeps only the empty-column dtype and string-offset-width fixes.
📝 WalkthroughWalkthroughThe proxy metaclass now controls when class-level attribute changes propagate to slow types. The pandas wrapper installs ChangescuDF pandas proxy mirroring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@python/cudf/cudf/pandas/fast_slow_proxy.py`:
- Around line 614-655: The teardown path in __setattr__ still forwards restored
proxy-local attributes back onto the real pandas class instead of restoring the
original slow implementation. Update __setattr__ on the fast/slow proxy class to
capture the pre-patch class attribute before type.__setattr__, and when
monkeypatch.undo() reassigns that same original proxy value (including plain
function/property-backed proxy members like DataFrame.eval and DataFrame.query),
treat it as a restore case by calling _fsproxy_restore_slow_attr(name) rather
than setattr(slow, name, value). Keep the existing handling for _MethodProxy,
_FastSlowAttribute, and _FastSlowProxy, but extend the restore detection to
cover the original proxy-local object identity.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 460c1729-97f2-4d18-b2b5-b7192cc9c92b
📒 Files selected for processing (4)
python/cudf/cudf/pandas/_wrappers/pandas.pypython/cudf/cudf/pandas/fast_slow_proxy.pypython/cudf/cudf/pandas/scripts/pandas-testing-plugin.pypython/cudf/cudf_pandas_tests/test_fast_slow_proxy.py
💤 Files with no reviewable changes (1)
- python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
vyasr
left a comment
There was a problem hiding this comment.
The implementation of setattr mirroring looks correct, but I'm not convinced that restoring via _fsproxy_restore_slow_attr represents the correct semantics. I'm guessing that this is meant to match pytest's monkeypatch fixture, but that's not how attribute setting/deleting should work in the general case unless I'm missing something.
|
/okay to test 835828b |
|
/okay to test c54ba21 |
…-attr mirroring Address review: __delattr__ now mirrors deletion as deletion (no restore), and the runtime patch stash (_fsproxy_slow_overrides/_fsproxy_restore_slow_attr) is gone. Instead, each proxy type snapshots its pristine public class attributes alongside the slow type's pristine class-dict entries when mirroring is enabled; __setattr__ translates assigned values into slow space: re-assigning the proxy's pristine attribute (what monkeypatch and mock.patch save and re-assign on undo) restores the slow type's pristine attribute, and proxy machinery unwraps to the slow object it delegates to. This also fixes real defects in the stash design's coverage: undo through unittest.mock (which saves the raw unresolved descriptor), patches of non-method attributes (properties, accessors, data attrs, cudf-installed attributes like DataFrame.columns/eval/query and Series.str, whose leak caused infinite recursion on the real type), classmethod/staticmethod descriptor preservation, and inherited methods no longer being copied into the slow type's dict on undo.
c54ba21 to
49a88e4
Compare
|
/okay to test 72b8804 |
|
/ok to test b17137b |
vyasr
left a comment
There was a problem hiding this comment.
I'm having a bit of trouble following all the cases when we actually mirror. I left some suggestions for improvement, then I'll take a second pass and hopefully I'll grok all paths.
| # delegates to, so save/patch/re-assign cycles round-trip on the | ||
| # real type as well. | ||
| type.__setattr__(cls, name, value) | ||
| if not cls.__dict__.get("_fsproxy_mirror_slow_overrides", False): |
There was a problem hiding this comment.
Can this attribute ever not exist? We create on construction, so I think we should be safe to access it unconditionally (I assume this was AI-generated, AIs tend to always prefer these safe constructions because they don't validate the invariants).
There was a problem hiding this comment.
Good instinct to question this — it flushed out a real bug. The attribute genuinely could be missing here, and the defensive lookup was silently papering over it: the flag was initialized in the metaclass __init__, but ABCMeta.__new__ (the ExcelFile/ExcelWriter proxies are built with metaclasses=(abc.ABCMeta,)) assigns __abstractmethods__ from inside __new__, which dispatches to this __setattr__ before __init__ ever runs. Switching to unconditional access made import cudf.pandas fail with AttributeError on ExcelWriter.
Fixed at the root: the flag is now initialized in _FastSlowProxyMeta.__new__ immediately after super().__new__(), so it exists before any cooperating metaclass can write class attributes, and the access here is unconditional as you suggested (with a comment documenting the ABCMeta ordering).
| # Mirroring is best-effort: translating a wrapped proxy instance | ||
| # can require a fast-to-slow conversion, which may itself fail; | ||
| # never let that escape an otherwise-successful assignment. | ||
| pristine = cls.__dict__.get("_fsproxy_pristine_attrs") or {} |
There was a problem hiding this comment.
| pristine = cls.__dict__.get("_fsproxy_pristine_attrs") or {} | |
| pristine = cls.__dict__.get("_fsproxy_pristine_attrs", {}) |
There was a problem hiding this comment.
Is pristine ever not set? Don't we guarantee it by calling _enable_fsproxy_mirroring?
There was a problem hiding this comment.
Superseded by the stronger form from your next comment: _fsproxy_pristine_attrs is guaranteed here, so it's now accessed unconditionally rather than defaulted.
There was a problem hiding this comment.
Correct — it's guaranteed: _enable_fsproxy_mirroring sets _fsproxy_pristine_attrs before it flips _fsproxy_mirror_slow_overrides to True, and this code is only reachable when the flag is True. Now accessed unconditionally. Same reasoning applied to _fsproxy_slow_type (it's in the class namespace at types.new_class time for every class the two make_*_proxy_type factories build), so the slow is None early-return is gone as well.
| else: | ||
| setattr(slow, name, entry[1]) | ||
| return | ||
| slow_value = _mirror_value_to_slow(value, slow, name, pristine) |
There was a problem hiding this comment.
This is the only place _mirror_value_to_slow is used. Since many of its branches are early returns, I suggest we inline it so those returns can happen directly. I think it will also make the logic here a bit easier to track, jumping between the call site and the definition is a bit confusing given how much more complex the proxying logic is getting. We can also drop _MIRROR_SKIP entirely as a type.
There was a problem hiding this comment.
Done — _mirror_value_to_slow is inlined into __setattr__ so each translation branch returns or mirrors directly at the call site, and _MIRROR_SKIP is gone. One nuance preserved from the helper: the classmethod/staticmethod descriptor-restore probe keeps its own inner try/except, so a raising __get__/__eq__ during the probe still falls back to mirroring the unwrapped function instead of aborting the mirror entirely.
| if entry[1] is _SLOW_ABSENT: | ||
| if name in slow.__dict__: | ||
| delattr(slow, name) |
There was a problem hiding this comment.
When do you hit this path? Shouldn't we avoid ever setting the attribute on the slow type in the first place?
There was a problem hiding this comment.
This is the undo half of a mirror we very much want to make. The proxy's class dict is built from dir(slow_type), so it holds pristine entries for names the slow type only inherits — e.g. DataFrame.head lives on NDFrame, and pandas.DataFrame.__dict__ has no 'head'. When a user patches pd.DataFrame.head, the mirror must set head on pandas.DataFrame itself: that shadowing entry is the only way fallback code resolving through the real class sees the patch. This branch runs when the patch is undone (monkeypatch re-assigns the saved pristine proxy descriptor): the slow-space translation of "restore pristine" for a slow-inherited name is "delete the shadowing entry we added", making the inherited implementation visible again. The name in slow.__dict__ guard covers the case where the original mirror never landed (mirroring is best-effort), so there's nothing to delete. Expanded the code comment with this example — test_class_attr_inherited_method_monkeypatch_roundtrip exercises exactly this cycle.
There was a problem hiding this comment.
Thanks for the detailed explanation, this makes sense now.
…ariants Initialize _fsproxy_mirror_slow_overrides in the metaclass __new__ rather than __init__: cooperating metaclasses can perform class-level attribute writes from their own __new__ (ABCMeta.__new__ assigns __abstractmethods__ for the ExcelFile/ExcelWriter proxies), dispatching to the mirroring __setattr__ before __init__ runs. With the flag guaranteed to exist, access it (and _fsproxy_slow_type/_fsproxy_pristine_attrs, both guaranteed once the flag is set by _enable_fsproxy_mirroring) unconditionally instead of via defensive cls.__dict__.get lookups. Also inline _mirror_value_to_slow into __setattr__ so its early returns read directly at the call site, dropping the _MIRROR_SKIP sentinel, and expand the comment on the _SLOW_ABSENT undo branch explaining why a mirrored patch for a slow-inherited attribute must be deleted on restore.
|
Addressed the review in ec3a6a3: Verified locally: |
|
/okay to test ec3a6a3 |
@galipremsagar, there was an error processing your request: See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/2/ |
|
/okay to test 791beab |
vyasr
left a comment
There was a problem hiding this comment.
OK, I think I've tracked all the paths here and they're doing the right things. Thanks for working through this.
| if entry[1] is _SLOW_ABSENT: | ||
| if name in slow.__dict__: | ||
| delattr(slow, name) |
There was a problem hiding this comment.
Thanks for the detailed explanation, this makes sense now.
|
/merge |
…IA#23001) Split out of NVIDIA#22927 per review. ## Problem A class-level attribute write on a cudf.pandas proxy type — e.g. `monkeypatch.setattr(pd.ExcelFile, "parse", fn)` — was only applied to the proxy class. Code that runs under `disable_module_accelerator()` (such as the pandas fallback path of `pd.read_excel`) resolves attributes from the *real* class, so a patch applied only to the proxy was invisible to it. ## Fix Add `_FastSlowProxyMeta.__setattr__`/`__delattr__` to mirror runtime class-level patches onto the underlying "slow" (real) type: - `__setattr__` mirrors the assignment after translating the assigned value into "slow" space. A plain value is forwarded as-is. Re-assigning the proxy's *pristine* attribute for a name (which is exactly what `monkeypatch.setattr` / `mock.patch.object` save and re-assign on undo) translates to the slow type's pristine attribute for that name — restored if the slow type had one of its own, or removed if it didn't (leaving any inherited implementation visible). Proxy machinery (e.g. a saved `pd.ExcelFile.parse`, a `_MethodProxy`) unwraps to the slow object it delegates to. - `__delattr__` mirrors a deletion as a deletion. Nothing is restored on delete. The pristine state is a per-type map `name -> (pristine proxy attribute, pristine slow class-dict entry)` snapshotted once, when `make_*_proxy_type` finishes building the type (the same point mirroring is enabled via `_fsproxy_mirror_slow_overrides`). It is a fixed translation table, not runtime patch tracking: there is no stash of "what to put back", and undo works for any code that follows the standard save/patch/re-assign pattern (pytest `monkeypatch`, `unittest.mock.patch.object`, manual saves) because the saved value itself identifies the pristine state. The translation is what makes mirroring safe at all — the values readable off a proxy type live in proxy space, and forwarding e.g. the saved `columns` property or `eval`/`query` functions verbatim onto `pandas.DataFrame` would install cudf machinery on the real class (for `columns` this infinitely recurses on the fallback path). cudf.pandas's own custom methods (`DataFrame.eval`/`query`) are installed via the new `_setattr_fsproxy_no_mirror` helper, which registers them as part of the proxy's pristine state without forwarding them to pandas. ## Tests Adds unit tests in `cudf_pandas_tests/test_fast_slow_proxy.py` covering: set/delete mirroring, monkeypatch round-trips (new attr, existing attr, nested, `delattr`), `mock.patch.object` (which saves the raw descriptor without resolving it), properties, plain data attributes, `staticmethod`/`classmethod` descriptor preservation, methods the slow type only inherits, and the no-mirror helper; plus an end-to-end test in `test_cudf_pandas.py` that patches/unpatches `DataFrame.columns`/`eval` and `Series.str` and checks real pandas is restored and functional. Removes 14 now-passing xfails from the pandas-tests plugin (13× `read_excel` engine-selection tests that monkeypatch the engine, plus a monkeypatch-registered custom accessor). The attribution of these 14 to the proxy fix (vs the Excel-reader fixes remaining in NVIDIA#22927) was verified locally by running each removed xfail with the proxy fix in isolation. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: NVIDIA#23001
Split out of #22927 per review.
Problem
A class-level attribute write on a cudf.pandas proxy type — e.g.
monkeypatch.setattr(pd.ExcelFile, "parse", fn)— was only applied to the proxy class. Code that runs underdisable_module_accelerator()(such as the pandas fallback path ofpd.read_excel) resolves attributes from the real class, so a patch applied only to the proxy was invisible to it.Fix
Add
_FastSlowProxyMeta.__setattr__/__delattr__to mirror runtime class-level patches onto the underlying "slow" (real) type:__setattr__mirrors the assignment after translating the assigned value into "slow" space. A plain value is forwarded as-is. Re-assigning the proxy's pristine attribute for a name (which is exactly whatmonkeypatch.setattr/mock.patch.objectsave and re-assign on undo) translates to the slow type's pristine attribute for that name — restored if the slow type had one of its own, or removed if it didn't (leaving any inherited implementation visible). Proxy machinery (e.g. a savedpd.ExcelFile.parse, a_MethodProxy) unwraps to the slow object it delegates to.__delattr__mirrors a deletion as a deletion. Nothing is restored on delete.The pristine state is a per-type map
name -> (pristine proxy attribute, pristine slow class-dict entry)snapshotted once, whenmake_*_proxy_typefinishes building the type (the same point mirroring is enabled via_fsproxy_mirror_slow_overrides). It is a fixed translation table, not runtime patch tracking: there is no stash of "what to put back", and undo works for any code that follows the standard save/patch/re-assign pattern (pytestmonkeypatch,unittest.mock.patch.object, manual saves) because the saved value itself identifies the pristine state. The translation is what makes mirroring safe at all — the values readable off a proxy type live in proxy space, and forwarding e.g. the savedcolumnsproperty oreval/queryfunctions verbatim ontopandas.DataFramewould install cudf machinery on the real class (forcolumnsthis infinitely recurses on the fallback path).cudf.pandas's own custom methods (
DataFrame.eval/query) are installed via the new_setattr_fsproxy_no_mirrorhelper, which registers them as part of the proxy's pristine state without forwarding them to pandas.Tests
Adds unit tests in
cudf_pandas_tests/test_fast_slow_proxy.pycovering: set/delete mirroring, monkeypatch round-trips (new attr, existing attr, nested,delattr),mock.patch.object(which saves the raw descriptor without resolving it), properties, plain data attributes,staticmethod/classmethoddescriptor preservation, methods the slow type only inherits, and the no-mirror helper; plus an end-to-end test intest_cudf_pandas.pythat patches/unpatchesDataFrame.columns/evalandSeries.strand checks real pandas is restored and functional.Removes 14 now-passing xfails from the pandas-tests plugin (13×
read_excelengine-selection tests that monkeypatch the engine, plus a monkeypatch-registered custom accessor). The attribution of these 14 to the proxy fix (vs the Excel-reader fixes remaining in #22927) was verified locally by running each removed xfail with the proxy fix in isolation.